﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace GameComponents
{
    public class InstancedMesh
    {
        const int MAX_PRIMITIVES = 1048575;

        private GraphicsDevice systemDevice;
        private VertexBuffer vertexBuffer;
        private IndexBuffer indexBuffer;
        private DynamicVertexBuffer matrixBuffer;
        private Matrix[] aryWorlds;
        private Color[] aryColors;
        public Effect Effect;

        private int primitivesPerMember;
        private int membersPerDraw;
        private int drawCount = 0;

        public InstancedMesh(GraphicsDevice newGraphicsDevice, Model newModel, Effect newEffect, int bufferCount)
        {
            if (bufferCount < 1)
            { throw new InvalidOperationException("Error: bufferCount out of range."); }
            
            systemDevice = newGraphicsDevice;
            Effect = newEffect;
            vertexBuffer = newModel.Meshes[0].MeshParts[0].VertexBuffer;
            indexBuffer = newModel.Meshes[0].MeshParts[0].IndexBuffer;
            primitivesPerMember = (int)(indexBuffer.IndexCount / 3);
            membersPerDraw = MAX_PRIMITIVES / primitivesPerMember;

            aryWorlds = new Matrix[bufferCount * membersPerDraw];
            aryColors = new Color[bufferCount * membersPerDraw];
            matrixBuffer = new DynamicVertexBuffer(systemDevice, InstanceVD, membersPerDraw * bufferCount, BufferUsage.WriteOnly);
        }

        // ***** ACCESSORS *****
        public bool AtMax { get { return (drawCount < aryWorlds.Length) ? true : false; } }
        public int DrawCount
        {
            get { return drawCount; }
            set
            {
                if ((value > -1) && (value <= aryWorlds.Length))
                { drawCount = value; }
                //else { throw new InvalidOperationException("Error drawCount out of range."); }
            }
        }
        public Matrix[] Worlds
        {
            get { return aryWorlds; }
            set
            {
                if (value.Length > aryWorlds.Length)
                { throw new InvalidOperationException("Error Too many world matricies for instanced mesh."); }
                else
                { value.CopyTo(aryWorlds, 0); }
            }
        }
        public Color[] Colors
        {
            get { return aryColors; }
            set
            {
                if (value.Length > aryColors.Length)
                { throw new InvalidOperationException("Error Too many colors for instanced mesh."); }
                else
                { value.CopyTo(aryColors, 0); }
            }
        }
        
        // ***** USER METHODS *****

        public void Draw()
        {
            if (drawCount > 0)
            {
                // Draw
                int drawRemaining = drawCount;
                while (drawRemaining > 0)
                {
                    if (drawRemaining > membersPerDraw)
                    {
                        systemDevice.SetVertexBuffers(null);
                        systemDevice.Indices = null;

                        // Refresh Data
                        matrixBuffer.SetData(0, aryWorlds, drawRemaining - membersPerDraw, membersPerDraw, InstanceVD.VertexStride, SetDataOptions.Discard);
                        matrixBuffer.SetData(64, aryColors, drawRemaining -  membersPerDraw, membersPerDraw, InstanceVD.VertexStride, SetDataOptions.Discard);

                        systemDevice.SetVertexBuffers(new VertexBufferBinding(vertexBuffer, 0, 0), new VertexBufferBinding(matrixBuffer, 0, 1));
                        systemDevice.Indices = indexBuffer;

                        // Draw
                        foreach (EffectPass passes in Effect.CurrentTechnique.Passes)
                        {
                            passes.Apply();
                            systemDevice.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, vertexBuffer.VertexCount, 0, primitivesPerMember, membersPerDraw);
                        }
                    }
                    else
                    {
                        systemDevice.SetVertexBuffers(null);
                        systemDevice.Indices = null;

                        // Refresh Data
                        matrixBuffer.SetData(0, aryWorlds, 0, drawRemaining, InstanceVD.VertexStride, SetDataOptions.Discard);
                        matrixBuffer.SetData(64, aryColors, 0, drawRemaining, InstanceVD.VertexStride, SetDataOptions.Discard);

                        systemDevice.SetVertexBuffers(new VertexBufferBinding(vertexBuffer, 0, 0), new VertexBufferBinding(matrixBuffer, 0, 1));
                        systemDevice.Indices = indexBuffer;

                        // Draw
                        foreach (EffectPass passes in Effect.CurrentTechnique.Passes)
                        {
                            passes.Apply();
                            systemDevice.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, vertexBuffer.VertexCount, 0, primitivesPerMember, drawRemaining);
                        }
                    }
                    drawRemaining -= membersPerDraw;
                }
            }
        }

        public void RefreshData()
        {
            //matrixBuffer.SetData(0, aryWorlds, 0, drawCount, InstanceVD.VertexStride, SetDataOptions.Discard);
            //matrixBuffer.SetData(64, aryColors, 0, drawCount, InstanceVD.VertexStride, SetDataOptions.Discard);
        }

        public void SetColor(int colorIndex, Color newColor)
        {
            if ((colorIndex > -1) && (colorIndex < aryColors.Length))
            { aryColors[colorIndex] = newColor; }
            //else { throw new InvalidOperationException("Error colorIndex out of range."); }
        }

        public void SetWorld(int worldIndex, Matrix newWorld)
        {
            if ((worldIndex > -1) && (worldIndex < aryWorlds.Length))
            { aryWorlds[worldIndex] = newWorld; }
            //else { throw new InvalidOperationException("Error worldIndex out of range."); }
        }

        static VertexDeclaration InstanceVD = new VertexDeclaration
        (
            new VertexElement(0, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 0),
            new VertexElement(16, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 1),
            new VertexElement(32, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 2),
            new VertexElement(48, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 3),
            new VertexElement(64, VertexElementFormat.Color, VertexElementUsage.Color, 0)
        );
    }
}
